# forward-compat boilerplate
from __future__ import absolute_import
from __future__ import with_statement
__metaclass__ = type

import re
import time
import uuid
from sqlalchemy import event
from sqlalchemy.ext.declarative import DeclarativeMeta


from fenton import util
from fenton import view
from fenton import model
from fenton import types
from fenton import getmeta
from fenton import timestamp
from fenton.db import METADATA, sql, orm


GUID_ATTR = '_object_guid'
COMMENT_ATTR = '__comment__'
DbModel = None


def make_code(name, corpus, r=re.compile('[^A-Z]+')):
    words = r.split(name.upper())
    n = len(words)
    start = n < 3 and 1 or 0
    indexes = [start] * n
    lens = [len(x) for x in words]
    word = len(words) - 1
    while indexes != lens:
        name = ''.join(p[:indexes[j]+1] for (j, p) in enumerate(words))
        if name not in corpus:
            return name
        if indexes[word] < lens[word]:
            indexes[word] += 1
        word = (word+1) % n
    suffix = 0
    while name in corpus:
        suffix += 1
        name += str(suffix)
    return name


def child_relation(child_ref, backref=None, **kw):
    if backref is not None:
        backref = '_%s_attached' % backref
    return orm.relation(child_ref,
                        backref=backref,
                        cascade='all,delete-orphan',
                        passive_deletes=True,
                        **kw)


def parent_property(*args, **kw):
    return ParentProperty(*args, **kw)


def seeds(Class):
    def decorator(f):
        _register_seed_func(Class, f)
        return f
    return decorator


def _insert_metainfo(cx, Class=None):
    if Class is None:
        Class = cx.Class
    classid = getmeta(Class).classid
    if not classid:
        return
    tablename = Class.__tablename__
    table = MetaObjectClass.__table__
    if table.select(table.c.classid==classid, bind=cx.bind).scalar():
        return
    print 'INSERT metadata (%s, %s)' % (classid, tablename)
    q = table.insert(bind=cx.bind)
    q.values(classid=classid, tablename=tablename).execute()


def _register_seed_func(Class, func):
    @event.listens_for(Class.__table__, 'after_create')
    def seed(table, bind, *x, **y):
        func(SeedContext(Class, table, bind))
        bind.db.flush()


def _print_create(t, *x, **y):
    print 'CREATE TABLE', t.name


def _itercsv(f, filter=None, skipheader=True):
    import csv
    header = 0
    for row in csv.reader(f):
        if skipheader and not header:
            header = 1
            continue
        if filter:
            row = filter(row)
        if row:
            yield row


def GUID():
    from sqlalchemy.dialects import postgresql
    return postgresql.UUID(as_uuid=True)


def _prepare_attrs(self, attrs):
    self.__attrs__ = attrs.get('__attrs__', {})

    for k, v in attrs.iteritems():
        if isinstance(v, tuple) and len(v)==1 and isinstance(v[0], types.Type):
            raise TypeError('errant comma at %s.%s' % (self.__name__, k))
        if isinstance(v, types.Type):
            v.instrument(self, k)
            self.__attrs__[k] = v

    fields = getattr(self, '__fields__', None) or ()
    if fields and isinstance(fields[0], basestring):
        fields = (('', '', fields),)
    self.__fields__ = fields


def _init_metainfo(self):
    if DbModel in self.__bases__ and GUID_ATTR not in self.__dict__:
        guid_col = sql.Column(GUID_ATTR, GUID(), nullable=False, unique=True)
        guid_col._creation_order = -1
        setattr(self, GUID_ATTR, guid_col)
        MI = MetaObjectInstance
        self._meta = orm.relation(MI,
                                  primaryjoin=(guid_col==MI.object_guid),
                                  foreign_keys=[MI.object_guid],
                                  #innerjoin=True,
                                  #lazy='joined',
                                  passive_deletes='all',
                                  uselist=False,
                                  viewonly=True,
                                  cascade=None)
        mattrs = self.__attrs__
        if COMMENT_ATTR not in mattrs and COMMENT_ATTR not in self.__dict__:
            setattr(self, COMMENT_ATTR, None)
            mattrs[COMMENT_ATTR] = types.Text(required=False,
                                              showview=False,
                                              label='Add comment')

    if getattr(self, COMMENT_ATTR, False) is not False:
        self.__fields__ = tuple(list(self.__fields__)
                                 + [(COMMENT_ATTR, ' ', (COMMENT_ATTR,))])


def _init_events(self):
    _register_seed_func(self, _insert_metainfo)
    event.listen(self, 'load', _set_context)
    event.listen(self.__table__, 'before_create', _print_create)


def _set_context(obj, _):
    m = getmeta(obj, False)
    if m:
        m.set_context(orm.object_session(obj).context, obj)




class ModelMetaclass(type):
    def __init__(self, name, bases, attrs):
        _prepare_attrs(self, attrs)
        return type.__init__(self, name, bases, attrs)




class DbMetaclass(DeclarativeMeta):
    def __init__(self, name, bases, attrs):
        superinit = DeclarativeMeta.__init__
        if DbModel is None:
            superinit(self, name, bases, attrs)
        else:
            _prepare_attrs(self, attrs)
            _init_metainfo(self)
            superinit(self, name, bases, attrs)
            _init_events(self)

    def __call__(Class):
        self = DeclarativeMeta.__call__(Class)
        if getattr(Class, GUID_ATTR, None) is not None:
            setattr(self, GUID_ATTR, uuid.uuid4())
        return self



class ParentProperty:

    def __init__(self):
        self.name = None

    attached_name = property(lambda x: '_%s_attached' % x.name)
    unattached_name = property(lambda x: '_%s_unattached' % x.name)

    def __compile(self, Class):
        for C in Class.mro():
            for k in C.__dict__:
                if C.__dict__.get(k) is self:
                    self.name = k
                    return
        raise TypeError('Name not found for parent_property on %s' % Class)

    def __get__(self, obj, Class):
        if self.name is None:
            self.__compile(Class)
        if obj is None:
            return getattr(Class, self.attached_name)
        return (getattr(obj, self.attached_name)
                or getattr(obj, self.unattached_name, None))

    def __set__(self, obj, x):
        if self.name is None:
            self.__compile(type(obj))
        setattr(obj, self.unattached_name, x)




class Lookup:

    __order_attr__ = 'label'

    def __title__(self):
        return getattr(self, self.__order_attr__, None) or '(new?)'

    @classmethod
    def __filter__(Class, q):
        return q.order_by(getattr(Class, Class.__order_attr__))

    @classmethod
    def symbols(Class, cx):
        key = getmeta(Class).key[0]
        db = getmeta(Class).get_db(cx)
        q = db.query(Class)
        return util.dictobject(dict((getattr(l, key), l) for l in q))

    def after_update_event(self, form):
        if not form:
            return
        M = getmeta(self)
        T = self.__table__
        C = T.c[M.key[0]]
        c = getattr(self, M.key[0])
        U = T.c._object_guid
        u = self._object_guid
        q = sql.select([sql.exists().where((C==c) & (U!=u))])
        if self._cx.db.execute(q).scalar():
            raise types.Invalid('That code is already used')

    def before_store_event(self):
        if not self._is_persistent():
            self.update_code()

    def update_code(self):
        key = getmeta(self).key[0]
        if getattr(self, key):
            return
        name = getattr(self, self.__order_attr__)
        if name:
            sel = sql.select([getattr(type(self), key)])
            codes = set(r[0] for r in self._db.execute(sel))
            code = make_code(name, codes)
        else:
            code = None
        setattr(self, key, code)




class TransientMeta(model.ModelMeta):
    def do_store_model(self, context, model):
        return model.do_store()
    def do_thaw_model(self, context, obj):
        super(TransientMeta, self).do_thaw_model(context, obj)
        m = getmeta(obj)
        for k in m.attributes:
            m.attributes[k].send_thaw(context, obj)




class DbMeta(model.DbMeta):
    get_db = lambda x,cx: cx.db

    def get_polymorphic_attr(self):
        mapper = orm.class_mapper(self.model_class)
        on = mapper.polymorphic_on
        return on is not None and on.key or None

    def get_polymorphic_id(self):
        mapper = orm.class_mapper(self.model_class)
        return mapper.polymorphic_identity

    def get_polymorphic_meta(self, polyid):
        mapper = orm.class_mapper(self.model_class)
        return getmeta(mapper.polymorphic_map[polyid].class_)

    # unused
    def polymogrify(self, obj):
        obj.__class__ = self.model_class
        from sqlalchemy.orm.attributes import instance_state, manager_of_class
        instance_state(obj).manager = manager_of_class(self.model_class)
        return obj

    # unused
    def get_polymorphic_keys(self):
        mapper = orm.class_mapper(self.model_class)
        return mapper.polymorphic_map.keys()

    # unused
    def get_polymorphic_update(self, obj, newid):
        mapper = orm.object_mapper(obj).base_mapper
        attr = self.get_polymorphic_attr()
        update = mapper.local_table.update().values({attr:newid})
        for col in mapper.primary_key:
            update = update.where(col == (getattr(obj, col.key)))
        return update




class ModelBase:
    _cx = property(lambda x:getmeta(x).get_context(x))
    _db = property(lambda x:x._cx.db)
    _rq = property(lambda x:x._cx.request)
    _meta = None

    def __eq__(self, other):
        if type(self) is not type(other):
            return False
        key = getmeta(self).key
        k1 = [getattr(self, k) for k in key]
        k2 = [getattr(other, k) for k in key]
        return k1 == k2

    def __str__(self):
        meta = getmeta(self)
        ids = [getattr(self, k, None) for k in meta.key]
        if all(x is not None for x in ids):
            title = getattr(self, '__title__', None)
            if isinstance(title, basestring):
                return title
            elif title:
                return title() or ''
            else:
                return ', '.join(map(str, ids))
        else:
            return meta.title

    def __repr__(self):
        return '%s(%s)' % (self.__class__.__name__, str(self))

    def set_attribute_event(self, key, value, form):
        f = getattr(self, 'set_attribute_'+key, None)
        if f:
            f(value, form)
        return bool(f)




@model.IterMeta.declare(metaclass=ModelMetaclass)
class Iter(ModelBase, model.ModelIterator):
    pass




@TransientMeta.declare(metaclass=ModelMetaclass)
class Transient(ModelBase):
    pass




@DbMeta.declare(metadata=METADATA, metaclass=DbMetaclass)
class DbModel(ModelBase):
    def set_attribute_event(self, key, value, form):
        if key == COMMENT_ATTR:
            self._cx.history_logger.log_comment(self, value)
            return True
        meta = getmeta(self)
        old = getattr(self, key)
        if not meta.attributes[key].is_equal(value, old):
            new_str = meta.attributes[key].format_value(value, self._cx)
            if old is None:
                old_str = '(None)'
            else:
                old_str = meta.attributes[key].format_value(old, self._cx)
            self._cx.history_logger.log_update(self, key, old_str, new_str)
        return ModelBase.set_attribute_event(self, key, value, form)

    def after_delete_event(self):
        self._cx.history_logger.log_delete(self)

    def _is_persistent(self):
        state = orm.attributes.instance_state(self)
        return state.key is not None




class Nav(Transient):
    __classid__ = None
    __view__ = view.NavScreen
    def get_links(self):
        for x in self.get_children():
            v = self._cx.get_view(x)
            if v.check() and v.get_url():
                yield v

    def get_children(self):
        return sorted([x for x in getmeta(self).get_children(self._cx)],
                      key=lambda x:str(x))




class CsvData(Transient):
    __view__ = view.Attachment
    def attachment_type(self):
        return 'text/csv; charset=UTF-8'

    def write_attachment_data(self, file):
        import csv
        w = csv.writer(file)
        for row in self.csv_rows():
            row = [unicode(x).encode('UTF-8') for x in row]
            w.writerow(row)




class Comment(Transient):
    __classid__ = None
    __edit__ = view.CommentScreen
    __fields__ = (
        'comment',
    )

    obj = types.String(editable=False)
    comment = types.Text(required=True)

    def do_store(self):
        self._cx.history_logger.log_comment(self.obj, self.comment)
        getmeta(self.obj).store_model(self._cx, self.obj)




class SeedContext:

    def __init__(self, Class, table, bind):
        self.meta = getmeta(Class)
        self.Class = Class
        self.table = table
        self.bind = bind
        self.db = bind.db
        self.context = bind.context
        self.app = bind.context.app

    def __getattr__(self, name):
        return getattr(self.context, name)

    def seed(self, data, *columns):
        data = [line.strip().split('|')
                for line in data.strip().splitlines()]
        if not columns:
            columns = data[0]
            data = data[1:]
        for row in data:
            self.insert(**dict(zip(columns, row)))

    def insert(self, **values):
        print 'INSERT', self.table.name, '\n      ',
        print ',\n       '.join('%s=%s'%v for v in values.items()
                                if v[0]!= GUID_ATTR)
        if self.meta.attributes:
            return self.meta.construct_model(self.context, **values)
        else:
            ins = self.table.insert(bind=self.bind)
            ins.values(values).execute()

    def run(self, *qs, **kw):
        rs = None
        for stmt in qs:
            print stmt
            rs = self.bind.execute(stmt, **kw)
        return rs

    def copy(self, rowiter, columns=None):
        has_guid = GUID_ATTR in self.table.c
        if has_guid:
            if columns:
                columns.append(GUID_ATTR)
            def addguid(rowiter):
                for row in rowiter:
                    yield list(row) + [str(uuid.uuid4())]
            rowiter = addguid(rowiter)

        if columns:
            columns = '(%s)' % ','.join(columns)

        for row in rowiter:
            values = {}
            for i, col in enumerate(self.table.columns):
                if col.key != GUID_ATTR:
                    values[col.key] = row[i] or None
            self.insert(**values)

    def copycsv(self, f, columns=None, filter=None, skipheader=True):
        csv = _itercsv(f, filter, skipheader)
        return self.copy(csv, columns)






### History stuff


class PendingEvent:
    action = comment = None
    def __init__(self):
        self.attrs = {}




class HistoryLogger:

    def __init__(self, context):
        self.context = context
        self.histories = {}

    def get_event(self, obj):
        classid = getmeta(obj).classid
        if not classid:
            return
        guid = getattr(obj, GUID_ATTR, None)
        if not guid:
            return
        e = self.histories.get(guid, None)
        if e is None:
            e = self.histories[guid] = PendingEvent()
            e.classid = classid
            e.guid = guid
            e.action = 'UPDATE'
            e.instance = obj._meta
            e.userid = self.context.user.username
        return e

    def log_update(self, obj, attr, old, new):
        e = self.get_event(obj)
        if e:
            e.attrs[attr] = (old, new)

    def log_comment(self, obj, text):
        e = self.get_event(obj)
        if e:
            e.comment = text is not None and text.strip() or None

    def log_delete(self, obj):
        e = self.get_event(obj)
        if e:
            e.action = 'DELETE'

    def store_event(self, pe, ts):
        instance = pe.instance
        action = pe.action
        if instance is None:
            action = 'CREATE'
            instance = MetaObjectInstance()
            instance.object_guid = pe.guid
            instance.classid = pe.classid
            instance.created_at = ts
            instance.created_by = pe.userid
        instance.updated_at = ts
        instance.updated_by = pe.userid
        ev = MetaObjectEvent()
        ev.action = action
        ev.timestamp = ts
        ev.userid = pe.userid
        ev.comment_text = pe.comment
        if pe.attrs:
            for k, (old, new) in pe.attrs.items():
                at = MetaObjectAttr()
                at.attr_name = k
                #at.attr_old = old
                #at.attr_new = new
                at.attr_value = new
                ev.attrs.append(at)
        ev.instance = instance
        getmeta(ev).store_model(self.context, ev)

    def flush(self):
        history = self.histories
        if not history:
            return
        self.histories = {}
        ts = timestamp.utcnow()
        for guid, event in history.iteritems():
            if not event.attrs:
                if event.comment:
                    event.action = 'COMMENT'
                elif event.action != 'DELETE':
                    continue
            self.store_event(event, ts)




class MetaObjectClass(DbModel):
    __classid__ = _object_guid = None
    __tablename__ = 'meta_object_class'
    classid = sql.Column(sql.String, primary_key=True)
    tablename = sql.Column(sql.String, nullable=False)
    instances = child_relation('MetaObjectInstance', back_populates='_class')
    meta = property(lambda x:DbMeta.meta_map[x.classid])




class MetaObjectInstance(DbModel):
    __classid__ = _object_guid = None
    __tablename__ = 'meta_object_instance'
    object_guid = sql.Column(GUID(), primary_key=True)
    version = sql.Column(sql.BigInteger(), nullable=False)
    classid = sql.Column(sql.String,
                         sql.ForeignKey('meta_object_class.classid'),
                         nullable=False)
    created_at = sql.Column(sql.DateTime(timezone=True), nullable=False)
    created_by = sql.Column(sql.String, nullable=False)
    updated_at = sql.Column(sql.DateTime(timezone=True), nullable=False)
    updated_by = sql.Column(sql.String, nullable=False)

    events = child_relation('MetaObjectEvent',
                            order_by=lambda:MetaObjectEvent.timestamp,
                            back_populates='instance')

    comments = property(lambda x:[e
                                  for e in x.events
                                  if e.comment_text])

    _class = orm.relation('MetaObjectClass', back_populates='instances')
    meta = property(lambda x:DbMeta.meta_map[x.classid])

    __mapper_args__ = {
        'version_id_col': version,
        'version_id_generator': lambda x:int(time.time() * 1000000)
    }

    def get_object(self):
        q = self._db.query(self.meta.model_class)
        return q.filter_by(_object_guid=self.object_guid).first()

    _obj = util.undefined
    @property
    def obj(self):
        if self._obj is util.undefined:
            self._obj = self.get_object()
        return self._obj




class MetaObjectEvent(DbModel):
    __classid__ = _object_guid = None
    __tablename__ = 'meta_object_event'
    object_guid = sql.Column(GUID(),
                             sql.ForeignKey('meta_object_instance.object_guid'),
                             primary_key=True)
    userid = sql.Column(sql.String(), primary_key=True)
    timestamp = sql.Column(sql.DateTime(timezone=True), primary_key=True)
    action = sql.Column(sql.String(), nullable=False)
    comment_text = sql.Column(sql.Text())
    instance = orm.relation('MetaObjectInstance', back_populates='events')
    attrs = child_relation('MetaObjectAttr', back_populates='event')

    obj = property(lambda x:x.instance.obj)
    meta = property(lambda x:x.instance.meta)

    @property
    def when(self):
        ts = self.timestamp.astimezone(self._cx.tz)
        return ts.strftime('%d/%m/%y %H:%M')





class MetaObjectAttr(DbModel):
    __classid__ = _object_guid = None
    __tablename__ = 'meta_object_attr'
    object_guid = sql.Column(GUID(), primary_key=True)
    userid = sql.Column(sql.String(), primary_key=True)
    timestamp = sql.Column(sql.DateTime(timezone=True), primary_key=True)
    attr_name = sql.Column(sql.String(), nullable=False, primary_key=True)
    attr_value = sql.Column(sql.String(), nullable=False)
    #attr_old = sql.Column(sql.String(), nullable=False)
    #attr_new = sql.Column(sql.String(), nullable=False)
    event = orm.relation('MetaObjectEvent', back_populates='attrs')
    __table_args__ = (sql.ForeignKeyConstraint(['userid', 'timestamp', 'object_guid'],
                                              ['meta_object_event.userid',
                                               'meta_object_event.timestamp',
                                               'meta_object_event.object_guid']),)

    @property
    def summary(self):
        return '%s: %s' % (self.label, self.attr_value or 'None')

    @property
    def meta(self):
        return self.event.instance.meta.attributes.get(self.attr_name)

    @property
    def label(self):
        return self.meta and self.meta.get_label() or self.attr_name




if 0:
    class MetaObjectDraft(DbModel):
        __classid__ = _object_guid = None
        __tablename__ = 'meta_object_draft'
        object_guid = sql.Column(GUID(), primary_key=True)
        userid = sql.Column(sql.String(), nullable=False)
        timestamp = sql.Column(sql.DateTime(timezone=True), nullable=False)
        data = sql.Column(sql.LargeBinary(), nullable=False)




    class MetaObjectLock(DbModel):
        __classid__ = _object_guid = None
        __tablename__ = 'meta_object_lock'
        object_guid = sql.Column(GUID(), primary_key=True)
        draft_guid = sql.Column(GUID(),
                                sql.ForeignKey('meta_object_draft.object_guid'),
                                nullable=False)




